// Copyright 2013 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dfa.axis.v1_19.matchtables;

import com.google.api.ads.dfa.axis.factory.DfaServices;
import com.google.api.ads.dfa.axis.v1_19.ApiException;
import com.google.api.ads.dfa.axis.v1_19.ContentCategory;
import com.google.api.ads.dfa.axis.v1_19.ContentCategoryRecordSet;
import com.google.api.ads.dfa.axis.v1_19.ContentCategoryRemote;
import com.google.api.ads.dfa.axis.v1_19.ContentCategorySearchCriteria;
import com.google.api.ads.dfa.axis.v1_19.Placement;
import com.google.api.ads.dfa.axis.v1_19.PlacementGroup;
import com.google.api.ads.dfa.axis.v1_19.PlacementGroupRecordSet;
import com.google.api.ads.dfa.axis.v1_19.PlacementGroupSearchCriteria;
import com.google.api.ads.dfa.axis.v1_19.PlacementRecordSet;
import com.google.api.ads.dfa.axis.v1_19.PlacementRemote;
import com.google.api.ads.dfa.axis.v1_19.PlacementSearchCriteria;
import com.google.api.ads.dfa.axis.v1_19.PlacementStrategy;
import com.google.api.ads.dfa.axis.v1_19.PlacementStrategyRecordSet;
import com.google.api.ads.dfa.axis.v1_19.PlacementStrategyRemote;
import com.google.api.ads.dfa.axis.v1_19.PlacementStrategySearchCriteria;
import com.google.api.ads.dfa.axis.v1_19.PricingPeriod;
import com.google.api.ads.dfa.axis.v1_19.PricingSchedule;
import com.google.api.ads.dfa.axis.v1_19.PricingType;
import com.google.api.ads.dfa.lib.client.DfaSession;

import java.rmi.RemoteException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Implementation of LookupObject that will generate Placement match tables.
 *
 * @author Moshe Kahan
 */
class PlacementLookup implements LookupObject {

  private PlacementRemote placementService;
  private ContentCategoryRemote contentCategoryService;
  private PlacementStrategyRemote placementStrategyService;

  private TableSettings tableSettings;

  private Set<Long> placementIdSet;
  private Set<String> timeSet;
  private int placementColumnIndex;
  private int timeColumnIndex;

  private Date dtFileStartDate;
  private Date dtFileEndDate;
  private final SimpleDateFormat timeFieldFormat;
  private final int dateStringStartIndex;
  private final int dateStringEndIndex;

  private Map<Long, String> pricingTypeMap;

  /**
   * Constructor accepts DFA service/session objects needed to create SOAP remotes to the
   * API.
   *
   * @param dfaServices DFA services object needed to create SOAP remotes to the API
   * @param session DFA session object needed to create SOAP remotes to the API
   * @param tableSettings match table application configuration used as a MatchTable object factory
   */
  PlacementLookup(DfaServices dfaServices, DfaSession session, TableSettings tableSettings) {
    // Request the various services from the service client factory.
    placementService = dfaServices.get(session, PlacementRemote.class);
    contentCategoryService = dfaServices.get(session, ContentCategoryRemote.class);
    placementStrategyService = dfaServices.get(session, PlacementStrategyRemote.class);
    this.tableSettings = tableSettings;
    pricingTypeMap = new HashMap<Long, String>();
    // Full string would be "MM-dd-yyyy-hh:mm:ss".  We can ignore hours, minutes, and seconds.
    String dateFormatString = "MM-dd-yyyy";
    dateStringStartIndex = 0;
    dateStringEndIndex = dateFormatString.length();
    timeFieldFormat = new SimpleDateFormat(dateFormatString);

    placementColumnIndex = -1;
    timeColumnIndex = -1;

    placementIdSet = new HashSet<Long>();
    timeSet = new HashSet<String>();
  }

  /**
   * Called when LookupObjectManager is parsing the DT file header. Method stores index
   * of column of interest.
   * @param index in DT file columns
   * @param header name of this DT file field/column
   */
  public void addHeader(int index, String header) throws DataTransferFileException {
    if (LookupField.PlacementId.getFieldName().equals(header)) {
      placementColumnIndex = index;
    }
    if (LookupObject.LookupField.Time.getFieldName().equals(header)) {
      timeColumnIndex = index;
    }
  }

  /**
   * Called to clear the DT header information' usually because you're processing a new file.
   */
  public void clearHeaders() {
    timeColumnIndex = -1;
    placementColumnIndex = -1;
  }

  /**
   * Called to parse the DT row.
   *
   * @param values array of tokenized Strings of DT file line
   */
  public void addValues(String[] values) throws DataTransferFileException {
    if (placementColumnIndex > -1) {
      String placementId = values[placementColumnIndex];
      if (placementId != null && !placementId.isEmpty() && !placementId.equals("0")) {
        placementIdSet.add(Long.valueOf(values[placementColumnIndex]));
      }
    }
    if (timeColumnIndex > -1) {
      try {
        // Parse only unique date-hour substrings for faster parsing.
        String dateString = values[timeColumnIndex].substring(
            dateStringStartIndex, dateStringEndIndex);
        if (!timeSet.contains(dateString)) {
          timeSet.add(dateString);
          Date date = timeFieldFormat.parse(dateString);
          if (dtFileStartDate == null || date.before(dtFileStartDate)) {
            dtFileStartDate = date;
          }
          if (dtFileEndDate == null || date.after(dtFileEndDate)) {
            dtFileEndDate = date;
          }
        }
      } catch (ParseException e) {
        throw new DataTransferFileException("Error parsing Time field in DT file", e);
      }
    }
  }

  /**
   * Called when parsing/aggregation is complete. Will lookup via SOAP all unique DFA entries
   * in the DT file and pass data to MatchTable class.
   *
   * @param outputPath location of match table to be generated.
   */
  public void generateMatchTable(String outputPath)
      throws MatchTableFileException, DfaRemoteException {

    // If Time column was absent from DT file, use system time.
    if (dtFileStartDate == null || dtFileEndDate == null) {
      dtFileEndDate = dtFileStartDate = new Date();
    }

    MatchTable pageMatchTable = tableSettings.createMatchTable(MatchTable.Type.PAGE);
    MatchTable pageCostMatchTable = tableSettings.createMatchTable(MatchTable.Type.PAGE_COST);
    MatchTable pageFlightCostMatchTable =
        tableSettings.createMatchTable(MatchTable.Type.PAGE_FLIGHT_COST);

    // Open match tables and write header row.
    if (pageMatchTable != null) {
      pageMatchTable.open(outputPath);
      pageMatchTable.printProgress();
    }

    if (pageCostMatchTable != null) {
      pageCostMatchTable.open(outputPath);
    }

    if (pageFlightCostMatchTable != null) {
      pageFlightCostMatchTable.open(outputPath);
    }

    SimpleDateFormat outputDateFormat = new SimpleDateFormat("yyyy-MM-dd");
    System.out.println("Using " + outputDateFormat.format(dtFileStartDate)
        + " for our 'current start' date.");
    System.out.println("Using " + outputDateFormat.format(dtFileEndDate)
        + " for our 'current end' date.");

    // Retrieve placements from SOAP API.
    SoapPageableLookup<Placement, PlacementSearchCriteria> placementSoapLookup =
        new SoapPageableLookup<Placement, PlacementSearchCriteria>(placementIdSet,
            new PlacementSoapLookupCallback());

    // Store placements and associated strategy, group, and content category IDs.
    List<Placement> placements = new ArrayList<Placement>();
    Map<Long, String> placementStrategyIdMap = new HashMap<Long, String>();
    Map<Long, PlacementGroup> placementGroupIdMap = new HashMap<Long, PlacementGroup>();
    Map<Long, String> contentCategoryIdMap = new HashMap<Long, String>();

    // Main placement SOAP lookup
    int placmentCount = 0;
    int placmentTotal = placementIdSet.size();
    while (placementSoapLookup.hasNext()) {
      Placement placement = placementSoapLookup.next();
      if (placement.getContentCategoryId() > 0) {
        contentCategoryIdMap.put(placement.getContentCategoryId(), null);
      }
      if (placement.getPlacementStrategyId() > 0) {
        placementStrategyIdMap.put(placement.getPlacementStrategyId(), null);
      }
      if (placement.getPlacementGroupId() > 0) {
        placementGroupIdMap.put(placement.getPlacementGroupId(), null);
      }
      placements.add(placement);

      // There can be a lot of placements.  Print progress every 10 or so.
      if (++placmentCount % 10 == 0) {
        System.out.printf("\r[%.1f%% of placement SOAP lookups completed]",
            100.0f * placmentCount / placmentTotal);
      }
    }
    if (placmentCount >= 100) {
      System.out.println("\r[100.0% of placement SOAP lookups completed]");
    }

    System.out.println("Executing secondary placement SOAP lookups");
    // Secondary ID lookups
    generateSecondaryLookups(placementStrategyIdMap, placementGroupIdMap, contentCategoryIdMap);

    SimpleDateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy");
    if (pageMatchTable != null) {
      for (Placement result : placements) {
        PricingSchedule pricingSchedule = result.getPricingSchedule();
        List<PricingPeriod> currentPricingPeriods = extractCurrentPricingPeriods(result);
        String placementType = extractPlacementType(result, placementGroupIdMap);
        String capCostOption = extractCapCostOption(result);

        // Multiple rows if multiple pricing periods.  Start/End Date for period.
        if (!currentPricingPeriods.isEmpty()) {
          for (PricingPeriod currentPricingPeriod : currentPricingPeriods) {
            Date startDate = currentPricingPeriod.getStartDate().getTime();
            Date endDate = currentPricingPeriod.getEndDate().getTime();
            pageMatchTable.write(
                Long.toString(result.getCampaignId()),
                Long.toString(result.getDfaSiteId()),
                Long.toString(result.getId()),
                result.getName(),
                contentCategoryIdMap.get(result.getContentCategoryId()),
                placementStrategyIdMap.get(result.getPlacementStrategyId()),
                startDate != null ? dateFormat.format(startDate) : "",
                endDate != null ? dateFormat.format(endDate) : "",
                placementType,
                Long.toString(result.getPlacementGroupId()),
                pricingSchedule != null ? pricingTypeMap.get(pricingSchedule.getPricingType()) : "",
                capCostOption,
                currentPricingPeriod != null ? Long.toString(currentPricingPeriod.getUnits()) : "",
                currentPricingPeriod != null ?
                    Double.toString(currentPricingPeriod.getRateOrCost()) : "",
                result.getPricingSchedule().isFlighted() ? "1" : "0");
          }
        } else {
          // One rows.  Start/End Date for entire schedule. 
          Date startDate = null;
          Date endDate = null;
          if (pricingSchedule != null) {
            Calendar startCalendar = pricingSchedule.getStartDate();
            Calendar endCalendar = pricingSchedule.getEndDate();
            if (startCalendar != null && endCalendar != null) {
              startDate = startCalendar.getTime();
              endDate = endCalendar.getTime();
            }
          }

          pageMatchTable.write(
              Long.toString(result.getCampaignId()),
              Long.toString(result.getDfaSiteId()),
              Long.toString(result.getId()),
              result.getName(),
              contentCategoryIdMap.get(result.getContentCategoryId()),
              placementStrategyIdMap.get(result.getPlacementStrategyId()),
              startDate != null ? dateFormat.format(startDate) : "",
              endDate != null ? dateFormat.format(endDate) : "",
              placementType,
              Long.toString(result.getPlacementGroupId()),
              pricingSchedule != null ? pricingTypeMap.get(pricingSchedule.getPricingType()) : "",
              capCostOption,
              "",
              "",
              result.getPricingSchedule().isFlighted() ? "1" : "0");
        }
      }
      pageMatchTable.flush();
      pageMatchTable.close();
    }

    // Flighted and Non-Flighted table have the same data, so store in array and then write.
    if (pageCostMatchTable != null || pageFlightCostMatchTable != null) {
      MatchTable.printProgress(pageCostMatchTable, pageFlightCostMatchTable);
      for (Placement result : placements) {
        PricingSchedule pricingSchedule = result.getPricingSchedule();
        if (pricingSchedule != null) {
          PricingPeriod[] pricingPeriods = pricingSchedule.getPricingPeriods();
          if (pricingPeriods != null) {
            Arrays.sort(pricingPeriods, new Comparator<PricingPeriod>() {
              public int compare(PricingPeriod period1, PricingPeriod period2) {
                  return period1.getStartDate().compareTo(period2.getStartDate());
              }
            });
            for (PricingPeriod pricingPeriod : pricingPeriods) {
              String[] fields = new String[] {
                  Long.toString(result.getId()),
                  dateFormat.format(pricingPeriod.getStartDate().getTime()),
                  dateFormat.format(pricingPeriod.getEndDate().getTime()),
                  Long.toString(pricingPeriod.getUnits()),
                  Double.toString(pricingPeriod.getRateOrCost()),
                  pricingPeriod.getComments()
              };
              if (pageCostMatchTable != null) {
                pageCostMatchTable.write(fields);
              }
              if (pageFlightCostMatchTable != null && pricingSchedule.isFlighted()) {
                pageFlightCostMatchTable.write(fields);
              }
            }
          }
        }
      }
      if (pageCostMatchTable != null) {
        pageCostMatchTable.flush();
        pageCostMatchTable.close();
      }
      if (pageFlightCostMatchTable != null) {
        pageFlightCostMatchTable.flush();
        pageFlightCostMatchTable.close();
      }
    }
  }

  // Does three SOAP lookups for aggregated IDs referred to by all Placement entities.
  private void generateSecondaryLookups(Map<Long, String> placementStrategyIdMap,
      Map<Long, PlacementGroup> placementGroupIdMap, Map<Long, String> contentCategoryIdMap)
          throws DfaRemoteException {

    // Lookup content category IDs.
    SoapPageableLookup<ContentCategory, ContentCategorySearchCriteria>
    contentCategorySoapLookup =
        new  SoapPageableLookup<ContentCategory, ContentCategorySearchCriteria>(
            contentCategoryIdMap.keySet(), new ContentCatgeorySoapLookupCallback());
    while (contentCategorySoapLookup.hasNext()) {
      ContentCategory contentCategory = contentCategorySoapLookup.next();
      if (contentCategory != null) {
        contentCategoryIdMap.put(contentCategory.getId(), contentCategory.getName());
      }
    }

    // Lookup strategy IDs.
    SoapPageableLookup<PlacementStrategy, PlacementStrategySearchCriteria>
    placementStrategySoapLookup =
        new  SoapPageableLookup<PlacementStrategy, PlacementStrategySearchCriteria>(
            placementStrategyIdMap.keySet(), new PlacementStrategySoapLookupCallback());
    while (placementStrategySoapLookup.hasNext()) {
      PlacementStrategy placementStrategy = placementStrategySoapLookup.next();
      if (placementStrategy != null) {
        placementStrategyIdMap.put(placementStrategy.getId(), placementStrategy.getName());
      }
    }

    // Lookup group IDs.
    SoapPageableLookup<PlacementGroup, PlacementGroupSearchCriteria> placementGroupSoapLookup =
        new SoapPageableLookup<PlacementGroup, PlacementGroupSearchCriteria>(
            placementGroupIdMap.keySet(), new PlacementGroupSoapLookupCallback());
    while (placementGroupSoapLookup.hasNext()) {
      PlacementGroup placementGroup = placementGroupSoapLookup.next();
      if (placementGroup != null) {
        placementGroupIdMap.put(placementGroup.getId(), placementGroup);
      }
    }
  }

  // Using start and end dates of DT file determine list of relevant pricing periods.
  private List<PricingPeriod> extractCurrentPricingPeriods(Placement placement) {
    List<PricingPeriod> currentPricingPeriods = new ArrayList<PricingPeriod>();
    // Find pricing period for the DT file.
    PricingSchedule pricingSchedule = placement.getPricingSchedule();
    if (pricingSchedule != null) {
      PricingPeriod[] pricingPeriods = pricingSchedule.getPricingPeriods();
      if (pricingPeriods != null) {
        for (PricingPeriod pricingPeriod : pricingPeriods) {
          if (pricingPeriod.getStartDate() != null && pricingPeriod.getEndDate() != null) {
            Calendar periodStartCalendar = Calendar.getInstance();
            Calendar periodEndCalendar = Calendar.getInstance();
            periodStartCalendar.setTime(pricingPeriod.getStartDate().getTime());
            periodEndCalendar.setTime(pricingPeriod.getEndDate().getTime());
            // Handle border cases
            periodStartCalendar.add(Calendar.DATE, -1);
            periodEndCalendar.add(Calendar.DATE, 1);

            Date periodStartDate = periodStartCalendar.getTime();
            Date periodEndDate = periodEndCalendar.getTime();
            if (periodStartDate != null && periodEndDate != null) {
              if (periodStartDate.before(dtFileStartDate)) {
                if (periodEndDate.after(dtFileStartDate)) {
                  currentPricingPeriods.add(pricingPeriod);
                }
              } else if (periodStartDate.before(dtFileEndDate)) {
                currentPricingPeriods.add(pricingPeriod);
              }
            }
          }
        }
      }
    }
    Collections.sort(currentPricingPeriods, new Comparator<PricingPeriod>() {
      public int compare(PricingPeriod period1, PricingPeriod period2) {
          return period1.getStartDate().compareTo(period2.getStartDate());
      }
    });
    return currentPricingPeriods;
  }

  // Map to placement type from placement group type.
  private String extractPlacementType(Placement placement,
      Map<Long, PlacementGroup> placementGroupIdMap) {
    String placementType = "";
    PlacementGroup placementGroup = placementGroupIdMap.get(placement.getPlacementGroupId());
    if (placementGroup != null) { 
      switch (placementGroup.getPlacementGroupType()) {
        case 1:
          placementType = "Package";
          break;
        case 2:
          placementType = "Roadblock";
          break;
        default:
          if (placement.getPlacementType() == 2 || placement.getPlacementType() == 4 ||
              placement.getPlacementType() == 9) {
            placementType = "Interstitial";
          } else {
            placementType = "Regular";
          }
          break;
      }
    }
    return placementType;
  }

  // Map cap cost option to String.
  private String extractCapCostOption(Placement placement) {
    String capCostOption = null;
    PricingSchedule pricingSchedule = placement.getPricingSchedule();
    if (pricingSchedule != null) {
      switch (pricingSchedule.getCapCostOption()) {
        case 1:
          capCostOption = "Monthly";
          break;
        case 2:
          capCostOption = "Monthly";
          break;
        default:
          capCostOption = "None";
          break;
      }
    }
    return capCostOption;
  }

  /* SoapLookupCallbacks */

  private class PlacementSoapLookupCallback
    implements SoapPageableLookupCallback<Placement, PlacementSearchCriteria> {

    public int getRecords(PlacementSearchCriteria criteria, List<Placement> recordList)
        throws ApiException, RemoteException {

      // Lazy initialization of pricingTypeMap
      if (pricingTypeMap.isEmpty()) {
        PricingType[] pricingTypes = placementService.getPricingTypes();
        for (PricingType pricingType : pricingTypes) {
          pricingTypeMap.put(pricingType.getId(), pricingType.getName());
        }
      }

      PlacementRecordSet records = placementService.getPlacementsByCriteria(criteria);
      int pages = records.getTotalNumberOfPages();
      for (Placement record : records.getRecords()) {
        recordList.add(record);
      }
      return pages;
    }

    public
    PlacementSearchCriteria getCriteria() {
      return new PlacementSearchCriteria();
    }
  }

  private class ContentCatgeorySoapLookupCallback
    implements SoapPageableLookupCallback<ContentCategory, ContentCategorySearchCriteria> {

    public int getRecords(ContentCategorySearchCriteria criteria, List<ContentCategory> recordList)
        throws ApiException, RemoteException {
      ContentCategoryRecordSet records = contentCategoryService.getContentCategories(criteria);
      int pages = records.getTotalNumberOfPages();
      for (ContentCategory record : records.getRecords()) {
        recordList.add(record);
      }
      return pages;
    }

    public ContentCategorySearchCriteria getCriteria() {
      return new ContentCategorySearchCriteria();
    }
  }

  private class PlacementStrategySoapLookupCallback
    implements SoapPageableLookupCallback<PlacementStrategy, PlacementStrategySearchCriteria> {

    public int getRecords(PlacementStrategySearchCriteria criteria,
        List<PlacementStrategy> recordList) throws ApiException, RemoteException {
      PlacementStrategyRecordSet records =
          placementStrategyService.getPlacementStrategiesByCriteria(criteria);
      int pages = records.getTotalNumberOfPages();
      for (PlacementStrategy record : records.getRecords()) {
        recordList.add(record);
      }
      return pages;
    }

    public PlacementStrategySearchCriteria getCriteria() {
      return new PlacementStrategySearchCriteria();
    }
  }

  private class PlacementGroupSoapLookupCallback
    implements SoapPageableLookupCallback<PlacementGroup, PlacementGroupSearchCriteria> {

    public int getRecords(PlacementGroupSearchCriteria criteria, List<PlacementGroup> recordList)
        throws ApiException, RemoteException {
      PlacementGroupRecordSet records =
          placementService.getPlacementGroupsByCriteria(criteria);
      int pages = records.getTotalNumberOfPages();
      for (PlacementGroup record : records.getRecords()) {
        recordList.add(record);
      }
      return pages;
    }

    public PlacementGroupSearchCriteria getCriteria() {
      return new PlacementGroupSearchCriteria();
    }
  }
}
